# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import abc
import math
from enum import IntEnum
from typing import Dict, List

from recordclass import recordclass

from coin.exchange.base.kr_rest.currency import NativeCurrency
from coin.exchange.base.kr_rest.product import NativeProduct
from coin.exchange.base.kr_rest.futures_product import NativeFuturesProduct
from coin.exchange.base.order_gateway_ready import OrderGatewayStatus, OrderGatewayReady
from coin.exchange.base.order_gateway_info import OrderGatewayInfo, BaseOrderGatewayInfoMutator


# TODO: OrderSide = coin.proto.coin_market_enums_pb2.OrderSide
# Do not confused with TradeSide.
# OrderSide is used in your order for Order Submit, Cancel, and Fill.
# TradeSide is used in market for Trade.
class OrderSide(IntEnum):
  UNKNOWN = 0
  BUY = 1
  SELL = 2


# TODO: OrderTye = coin.proto.coin_market_enums_pb2.OrderType
class OrderType(IntEnum):
  UNKNOWN = 0
  LIMIT = 1
  IOC = 2
  FOK = 3
  MARKET = 4


Order = recordclass(
    'Order',
    [
        'order_id',  # type: str
        'product',  # type: Product
        'price',  # type: float
        'qty',  # type: float
        'order_side',  # type: OrderSide
        'order_type',  # type: OrderType
        'tag',  # type: Any
        'accepted',  # type: bool
        'cancel_sent',  # type: bool
        'internal'
    ])  # type: Any


def are_orders_equivalent(order_1, order_2):
  return (order_1.product == order_2.product and math.isclose(order_1.price, order_2.price)
          and math.isclose(order_1.qty, order_2.qty) and order_1.order_side == order_2.order_side
          and order_1.order_type == order_2.order_type)


def assert_orders_equivalent(order_1, order_2, rel_tol=1e-6, abs_tol=1e-8):
  assert order_1.product == order_2.product, (order_1.product, order_2.product)
  assert math.isclose(order_1.price, order_2.price, rel_tol=rel_tol,
                      abs_tol=abs_tol), (order_1.price, order_2.price)
  assert math.isclose(order_1.qty, order_2.qty, rel_tol=rel_tol,
                      abs_tol=abs_tol), (order_1.qty, order_2.qty)
  assert order_1.order_side == order_2.order_side, (order_1.order_side, order_2.order_side)
  assert order_1.order_type == order_2.order_type, (order_1.order_type, order_2.order_type)


class OrderGatewayBase(object):
  __metaclass__ = abc.ABCMeta

  _market_type = None
  _exchange = None
  _api_version = None

  OGInfoType = None
  ProductType = None

  def __init__(self):
    self._og_info = None
    self._og_ready = OrderGatewayReady()
    self._fill_callbacks = []
    self._og_logger = None
    self._latency_recorder = None

  def set_log_write_callback(self, callback):
    return self._og_logger.set_write_callback(callback)

  def set_latency_recorder(self, latency_recorder):
    self._latency_recorder = latency_recorder

  @property
  def market_type(self):
    return self._market_type

  @property
  def exchange(self):
    return self._exchange

  @property
  def api_version(self):
    return self._api_version

  @property
  def og_info(self):
    return self._og_info

  @property
  def og_logger(self):
    return self._og_logger

  def is_ready(self):
    return self._og_ready.is_ready()

  @property
  def status(self) -> OrderGatewayStatus:
    return self._og_ready.status

  @property
  def _status(self) -> OrderGatewayStatus:
    return self._og_ready.status

  def get_balance(self, currency) -> float:
    return self._og_info.get_balance(currency)

  def get_position(self, product) -> float:
    return self._og_info.get_position(product)

  def get_balances(self) -> Dict[str, float]:
    return self._og_info.get_balances()

  def get_positions(self) -> Dict[str, float]:
    return self._og_info.get_positions()

  def get_balances_copy(self) -> Dict[str, float]:
    return self._og_info.get_balances()

  def get_positions_copy(self) -> Dict[str, float]:
    return self._og_info.get_positions()

  def get_working_order(self) -> List[Order]:
    # Return list of Order
    raise NotImplementedError()

  def liquidity_pool_submit(self,
             product,
             **kwargs):
    assert isinstance(product, (NativeProduct, NativeFuturesProduct)), type(product)
    assert isinstance(product, self.OGInfoType.ProductType), type(product)

    ret = self.liquidity_pool_submit_impl(product=product,**kwargs)
    return ret

  def submit(self,
             product,
             price,
             qty,
             order_side,
             order_type,
             *,
             order_id=None,
             tag=None,
             **kwargs):
    assert isinstance(product, (NativeProduct, NativeFuturesProduct)), type(product)
    assert isinstance(product, self.OGInfoType.ProductType), type(product)

    assert type(price) == float, type(price)
    # spot = float
    # futures = int
    assert isinstance(qty, (float, int)), type(qty)
    # TODO: support client specific order type
    # assert type(order_side) == OrderSide, type(order_side)
    # assert type(order_type) == OrderType, type(order_type)
    if order_id is not None:
      assert type(order_id) == str, type(order_id)
    if tag is not None:
      assert type(tag) == str, type(tag)

    ret = self.submit_impl(product,
                           price,
                           qty,
                           order_side,
                           order_type,
                           order_id=order_id,
                           tag=tag,
                           **kwargs)

    return ret

  def cancel(self, order_id, *, ignore_error=False):
    assert type(order_id) == str, type(order_id)
    return self.cancel_impl(order_id, ignore_error=ignore_error)

  def cancel_multiple(self, order_ids, *, ignore_error=False):
    assert type(order_ids) == list, type(order_ids)
    return self.cancel_multiple_impl(order_ids, ignore_error=ignore_error)

  def cancel_product(self, product, *, ignore_error=False):
    assert isinstance(product, (NativeProduct, NativeFuturesProduct))
    assert isinstance(product, self.OGInfoType.ProductType)
    return self.cancel_product_impl(product, ignore_error=ignore_error)

  def cancel_all(self, *, ignore_error=False):
    return self.cancel_all_impl(ignore_error=ignore_error)

  def register_fill_callback(self, callback):
    # check callback
    self._fill_callbacks.append(callback)

  def _publish_fill(self, price, qty, order):
    for callback in self._fill_callbacks:
      try:
        callback(price, qty, order)
      except Exception as e:
        self._logger.exception('ERROR: publish_fill error: %s %s %s %s', price, qty, order, e)

  @abc.abstractmethod
  def submit_impl(self, product, price, qty, order_side, order_type, *, order_id=None, tag=None):
    raise NotImplementedError()

  @abc.abstractmethod
  def cancel_impl(self, order_id, *, ignore_error):
    raise NotImplementedError()

  @abc.abstractmethod
  def liquidity_pool_submit_impl(self, product,**kwargs):
    raise NotImplementedError()
  
  # @abc.abstractmethod
  def cancel_product_impl(self, product, *, ignore_error):
    raise NotImplementedError()

  # @abc.abstractmethod
  def cancel_all_impl(self, *, ignore_error):
    raise NotImplementedError()

  # @abc.abstractmethod
  def cancel_multiple_impl(self, order_ids, *, ignore_error):
    raise NotImplementedError()
